// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using SnapObjects.Commons;
using System.Threading;
using System.Threading.Tasks;

namespace SnapObjects.Data
{
    internal class ModelLoader : IModelLoader
    {
        private readonly DataContext _context;
        private readonly ModelSelectBuilder _modelSelectBuilder;
        private readonly ModelSqlExecutor _modelSqlExecutor;

        public ModelLoader(DataContext context)
        {
            _context = context;
            _modelSelectBuilder = new ModelSelectBuilder(context);
            _modelSqlExecutor = new ModelSqlExecutor(context);
        }

        public ISqlQueryBuilder GetQueryBuilder<TModel>(string selectName = null)
        {
            return _modelSelectBuilder.GetModelQueryBuilder(typeof(TModel), selectName)
                                        .SqlQueryBuilder;
        }

        public ILoadable<TModel> Load<TModel>(params object[] parameters)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .Load(parameters);
        }

        public Task<ILoadable<TModel>> LoadAsync<TModel>(object[] parameters, CancellationToken cancellationToken)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadAsync(parameters, cancellationToken);
        }

        public ILoadable<TModel> LoadByPage<TModel>(
            int currentIndex,
            int pageSize,
            params object[] parameters)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadByPage(currentIndex, pageSize, parameters);
        }

        public Task<ILoadable<TModel>> LoadByPageAsync<TModel>(
            int currentIndex,
            int pageSize,
            object[] parameters,
            CancellationToken cancellationToken)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadByPageAsync(currentIndex, pageSize, parameters, cancellationToken);
        }

        public ILoadable<TModel> LoadAll<TModel>()
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadAll();
        }

        public Task<ILoadable<TModel>> LoadAllAsync<TModel>(CancellationToken cancellationToken)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadAllAsync(cancellationToken);
        }

        public ILoadable<TModel> LoadAllByPage<TModel>(int currentIndex, int pageSize)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadAllByPage(currentIndex, pageSize);
        }

        public Task<ILoadable<TModel>> LoadAllByPageAsync<TModel>(
            int currentIndex, int pageSize, CancellationToken cancellationToken)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadAllByPageAsync(currentIndex, pageSize, cancellationToken);
        }

        public ILoadable<TModel> LoadByKey<TModel>(params object[] parameters)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadByKey(parameters);
        }

        public Task<ILoadable<TModel>> LoadByKeyAsync<TModel>(object[] parameters, CancellationToken cancellationToken)
        {
            return new ModelLoadable<TModel>(_context, _modelSelectBuilder)
                        .LoadByKeyAsync(parameters, cancellationToken);
        }

        public IEmbeddedLoadable<TModel> LoadEmbedded<TModel>(TModel model, params object[] parameters)
        {
            Check.NotNull(model, nameof(model));

            var modelMeta = ModelMetaManager.GetModelMeta(typeof(TModel));
            var adoDbParams = ModelParameterHelper.BindParameters(
                                _context,
                                modelMeta.SqlParameters,
                                parameters);

            return new EmbeddedLoadable<TModel>(
                                _context,
                                _modelSelectBuilder,
                                model,
                                (IReadOnlyList<IAdoDbParameter>)adoDbParams);
        }

        public bool Exists<TModel>(params object[] parameters)
        {
            return this.Count<TModel>(parameters) > 0;
        }

        public bool KeyExists<TModel>(params object[] parameters)
        {
            var sqlText = _modelSelectBuilder.BuildSelectCountByKey(
                                                    typeof(TModel),
                                                    out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            var count = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            var value = ValueConvert.Convert<int>(count); 

            return value > 0;
        }

        public int Count<TModel>(params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.SelectCount,
                                null,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            var count = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            var value = ValueConvert.Convert<int>(count);

            return value;
        }

        public int DistinctCount<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne
                                (typeof(TModel),
                                QuerySelector.SelectDistinctCount,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                        _context,
                                                        sqlParameters,
                                                        parameters);

            var count = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            var value = ValueConvert.Convert<int>(count);

            return value;
        }

        public object Max<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.SelectMax,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            var max = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            return max;
        }

        public object Min<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.SelectMin,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                        _context,
                                                        sqlParameters,
                                                        parameters);

            var min = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            return min;
        }

        public object Avg<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.SelectAvg,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                        _context,
                                                        sqlParameters,
                                                        parameters);

            var avg = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            return avg;
        }

        public object Sum<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.SelectSum,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            var sum = _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);

            return sum;
        }

        public object FetchOne<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.Scalar,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            return _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);
        }

        public Task<TValue> FetchOneAsync<TModel, TValue>(
            string expression, object[] parameters, CancellationToken cancellationToken)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.Scalar,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            return _modelSqlExecutor.ScalarAsync<TValue>(sqlText, adoDbParameters, cancellationToken);
        }

        public object FetchOneByKey<TModel>(string expression, params object[] parameters)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.ScalarByKey,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            return _modelSqlExecutor.Scalar<object>(sqlText, adoDbParameters);
        }

        public Task<TValue> FetchOneByKeyAsync<TModel, TValue>(
            string expression, object[] parameters, CancellationToken cancellationToken)
        {
            var sqlText = this.BuildFetchOne(
                                typeof(TModel),
                                QuerySelector.ScalarByKey,
                                expression,
                                out var sqlParameters);

            var adoDbParameters = ModelParameterHelper.BindParameters(
                                                            _context,
                                                            sqlParameters,
                                                            parameters);

            return _modelSqlExecutor.ScalarAsync<TValue>(sqlText, adoDbParameters, cancellationToken);
        }

        #region Private methods
        private string BuildFetchOne(
            Type modelType,
            QuerySelector modelSql,
            string expression,
            out IReadOnlyList<ISqlParameter> definedParameters)
        {
            var sqlText = _modelSelectBuilder.BuildFetchOne(
                                            modelType,
                                            modelSql,
                                            expression,
                                            out var sqlParameters);

            definedParameters = sqlParameters;

            return sqlText;
        }
        #endregion
    }
}
